package src;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import src.mock.tomcat.MockServlet;

public class NIOWebServer {
    private final int PORT;
    private final WorkerConfigure workerConfigure;
    private final ExecutorService threadPool;
    public static final String WorkerConfigKey = "workerConfig";
    public static final String WebConfigKey = "webConfig";
    public static final String ThreshHoldKey = "queueThreshHold";
    /**
     * 记录urlMapping信息，这部分配置信息可以从webmapping.xml中读取，key为path-method
     */
    private final HashMap<String, WebConfigEntry> urlMapping = new HashMap<String, WebConfigEntry>();
    /**
     * 动态生成json内容，类似SpringMVC的Controller，key为path-method
     */
    private final HashMap<String, Object> controllerMap = new HashMap<String, Object>();
    /**
     * 静态文件缓存，key作为文件名
     */
    private final HashMap<String, String> fileCache = new HashMap<String, String>();
    /**
     * 由于无法提供Spring DI的功能，所以此处的Metric就用简单的全局域实现
     */
    protected static final HashMap<String, String> metricInfo = new HashMap<String, String>();

    /**
     * 线程池配置
     */
    static class WorkerConfigure {
        private final static int DEFAULT_THREAD_POOL_SIZE = 10;
        private static final int DEFAULT_TASK_QUEUE_SIZE = 1000;
        private final int TASK_QUEUE_SIZE;
        private final int CORE_POOL_SIZE;
        private final int MAX_POOL_SIZE;
        private final int WARN_THRESHOLD;

        /**
         * WorkerConfig，包括工作线程池的详细配置，新增了一个有界队列+动态监测队列长度的功能
         */
        public WorkerConfigure() {
            this(DEFAULT_THREAD_POOL_SIZE, DEFAULT_THREAD_POOL_SIZE, DEFAULT_TASK_QUEUE_SIZE,
                    (int) (DEFAULT_TASK_QUEUE_SIZE * 0.8));
        }

        public WorkerConfigure(int coreThreadPoolSize, int maxThreadPoolSize, int taskQueueSize, int warnThreshold) {
            if (coreThreadPoolSize < 0 || maxThreadPoolSize < coreThreadPoolSize) {
                throw new IllegalArgumentException(String.format("线程池参数设置异常，核心线程数：%d，最大线程数:%d\n",
                        coreThreadPoolSize, maxThreadPoolSize));
            }
            this.TASK_QUEUE_SIZE = taskQueueSize;
            this.CORE_POOL_SIZE = coreThreadPoolSize;
            this.MAX_POOL_SIZE = maxThreadPoolSize;
            this.WARN_THRESHOLD = warnThreshold;
        }

        public String toString() {
            return String.format("核心线程数%s，最大线程数：%s，任务队列长度:%b，队列告警阈值：%s\n",
                    this.CORE_POOL_SIZE, this.MAX_POOL_SIZE, this.TASK_QUEUE_SIZE, this.WARN_THRESHOLD);
        }
    }

    // TODO 后续改为从xml里面读取配置 本质是一个DOM解析过程
    public void initialUrlMapping() {
        // 静态资源
        urlMapping.put("/nio-GET", new WebConfigEntry("nio", "GET", false, "resources/nio.html", null, null));
        urlMapping.put("/server-GET", new WebConfigEntry("server", "GET", false, "resources/server.html", null, null));
        urlMapping.put("/-GET", new WebConfigEntry("index.html", "GET", false, "resources/index.html", null, null));
        urlMapping.put("/index.html-GET",
                new WebConfigEntry("index.html", "GET", false, "resources/index.html", null, null));
        urlMapping.put("/git-GET", new WebConfigEntry("git", "GET", false, "resources/git.html", null, null));
        // 动态资源，支持metric接口，目前支持了
        urlMapping.put("/metric-GET", new WebConfigEntry("metric", "GET", true, null, "src.MetricService", "doGet"));
    }

    /**
     * webserver路径配置，基于RESTful分格，按资源与操作
     */
    class WebConfigEntry {
        String requiredResource; // 请求的资源
        String operateMethod; // 操作资源的方式 例如GET、PUT、PATCH
        boolean dynamic; // 是否是动态资源
        String serverResource; // 服务器资源
        String controllerPath; // 动态资源的controller
        String controllerMethod; // 动态资源的method

        public WebConfigEntry(String requiredResource, String operateMethod, boolean dynamic,
                String serverResource, String controllerPath, String controllerMethod) {
            this.requiredResource = requiredResource;
            this.operateMethod = operateMethod;
            this.dynamic = dynamic;
            this.serverResource = serverResource;
            this.controllerPath = controllerPath;
            this.controllerMethod = controllerMethod;
        }

        public String toString() {
            return String.format("请求资源：%s，操作方法：%s，是否是静态资源:%b，服务器资源对应路径：%s，动态资源controller路径:%s，动态资源对应方法:%s\n",
                    this.requiredResource, this.operateMethod, this.dynamic, this.serverResource, this.controllerPath,
                    this.controllerMethod);
        }
    }

    /**
     * 线程池的设置要更精细，因为默认任务队列的大小是Inter.MAX_VALUE，在Java语言中
     * Integer的实现是有符号数：2的31次方-1，近似为20亿，所以正常情况下不会触发任务队列已满的拒绝策略
     * 更多是会以OutOfMemory、FD耗尽、请求延迟递增而为人感知，所以需要结合业务场景设置线程池参数
     */
    public NIOWebServer(int port, WorkerConfigure workerConfigure) {
        this.PORT = port;
        this.workerConfigure = workerConfigure;
        this.threadPool = new ThreadPoolExecutor(
                this.workerConfigure.CORE_POOL_SIZE,
                this.workerConfigure.MAX_POOL_SIZE,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(this.workerConfigure.TASK_QUEUE_SIZE), // 有界队列
                new ThreadPoolExecutor.AbortPolicy() // 直接拒绝，配合动态监测队列积压
        );
        // 初始化资源路径配置
        initialUrlMapping();
        // 指标初始化
        initMetric();
    }

    public void initMetric() {
        StringBuilder urlMappingBuilder = new StringBuilder();
        for (String key : this.urlMapping.keySet()) {
            urlMappingBuilder.append(String.format("key:%s,value:%s", key, this.urlMapping.get(key).toString()));
        }
        this.metricInfo.put(this.WebConfigKey, urlMappingBuilder.toString());
        this.metricInfo.put(this.WorkerConfigKey, this.workerConfigure.toString());
    }

    public void start() throws IOException {
        /**
         * NIO固定步骤，根据底层操作系统实现SPI机制获取select实现
         */
        // 1. 创建Selector
        Selector selector = Selector.open();
        // 2. 创建ServerSocketChannel并绑定端口
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(this.PORT));
        serverSocketChannel.configureBlocking(false);
        // 3. 注册ServerSocketChannel到Selector，监听ACCEPT事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIOWebServer started on port " + PORT);
        while (true) {
            // 4. 阻塞等待就绪的Channel
            selector.select();
            // 5. 获取就绪的SelectionKey集合 底层操作系统fd集合的子集，减少FD在内核态和用户态的拷贝
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
            // 6. 循环处理
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                keyIterator.remove();
                // 防止出现CancelledKeyException异常
                if (!key.isValid()) {
                    continue;
                }
                // 底层FD对应的标志位
                try {
                    if (key.isAcceptable()) {
                        handleAccept(key, selector);
                    } else if (key.isReadable()) {
                        handleRead(key);
                    }
                } catch (CancelledKeyException e) {
                    // 记录日志但不需要处理
                    System.err.println("Key already cancelled: " + e.getMessage());
                } catch (IOException e) {
                    key.cancel();
                    try {
                        if (key.channel().isOpen()) {
                            key.channel().close();
                        }
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }

    private void handleAccept(SelectionKey key, Selector selector) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = serverChannel.accept();
        clientChannel.configureBlocking(false);
        // 注册读事件
        clientChannel.register(selector, SelectionKey.OP_READ);
        System.out.println("New client connected: " + clientChannel.getRemoteAddress());
    }

    private void handleRead(SelectionKey key) {
        // 先检查有效性
        if (!key.isValid()) {
            return;
        }
        // 提交到线程池处理
        this.threadPool.execute(() -> {
            // 队列长度判断，可触发告警与限流
            if (((ThreadPoolExecutor) threadPool).getQueue().size() > this.workerConfigure.WARN_THRESHOLD) {
                synchronized (this.metricInfo) {
                    this.metricInfo.put(this.ThreshHoldKey,
                            String.format("触发告警:%d\n", ((ThreadPoolExecutor) threadPool).getQueue().size()));
                }
            }
            // 此处为拿到的底层FD，由线程池里面的线程负责交互IO细节
            SocketChannel clientChannel = (SocketChannel) key.channel();
            if (!clientChannel.isOpen()) {
                key.cancel();
                try {
                    if (key.channel().isOpen()) {
                        key.channel().close();
                    }
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                /**
                 * key cancel之后，关于key的有效性在外部进行判断
                 * selector.selectNow();
                 */
                return;
            }
            /**
             * JVM提供的bytebuffer，有两个关键操作和一个函数
             * limit
             * position
             * 其中[position,limit]之间的属于是可以写入/读取的部分
             */
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            StringBuilder requestBuilder = new StringBuilder();
            /**
             * 基于复用charbuffer的操作
             * ByteBuffer → CharsetDecoder → CharBuffer → StringBuilder
             * 
             */
            Charset charset = StandardCharsets.UTF_8;
            // 设置解码器，避免抛出异常
            CharsetDecoder decoder = charset.newDecoder()
                    // CharsetDecoder 有严格的状态流转：RESET → CODING → FLUSHED → END
                    .onMalformedInput(CodingErrorAction.REPLACE)
                    .onUnmappableCharacter(CodingErrorAction.REPLACE);
            CharBuffer charBuffer = CharBuffer.allocate(1024);

            /**
             * 基于复用byte数组的操作
             * byte[] reusableBytes = new byte[buffer.capacity()];
             * buffer.get(reusableBytes,0,bytesRead);
             * requestBuilder.append(reusableBytes,0,bytesRead);
             */
            try {
                /**
                 * 循环读取，通过系统调用read函数读取数据，返回值为读取到的数据量
                 */
                while (true) {
                    // 重置buffer以便复用
                    byteBuffer.clear();
                    int bytesRead = clientChannel.read(byteBuffer);
                    // 连接已关闭
                    if (bytesRead == -1) {
                        clientChannel.close();
                        break;
                        // 当前无数据可读（非阻塞模式下可能发生）
                    } else if (bytesRead == 0) {
                        break;
                    } else {
                        // 切换读模式，从[position,limit]这段区间中读取数据
                        byteBuffer.flip();
                        // 循环解码 in out
                        while (byteBuffer.hasRemaining()) {
                            // 解码循环
                            CoderResult result = decoder.decode(byteBuffer, charBuffer, false);
                            if (result.isOverflow()) {
                                // 1. 先输出已解码内容
                                charBuffer.flip();
                                requestBuilder.append(charBuffer); // 这里判断是ok的，因为是命中了isOverflow
                                charBuffer.clear();
                                // 2. 双倍扩容，触发条件判断
                                if (byteBuffer.remaining() > charBuffer.capacity()) {
                                    charBuffer = CharBuffer.allocate(charBuffer.capacity() * 2);
                                }
                                // 3. 重新尝试解码
                                continue;
                            } else if (result.isUnderflow()) {
                                /**
                                 * 从input中[position,limit]拷贝至[0,limit-position]，方便后续的decode
                                 * 这里等待charbuffer满了之后再进行一次操作
                                 * 为什么这里需要一次compact，并且循环处理后byteBuffer不能clear呢？
                                 * 因为如果是单字符编码，例如英文，按单个字节截取不会出问题，但多个字节的编码，
                                 * 如中文，可能存在一个字符分到两个缓冲区的情况，例如nio.html遇到的
                                 * <p>
                                 * <code>flip()</code> 方法主要完成如下两步：
                                 * </p>
                                 * ，在"两步"这个地方，字节数来到1024的倍数
                                 * 然后后续的字符被截取到两个缓冲区中，如果不进行compact且不clear，这个“步的"编码(&#x6B65)就会
                                 * 丢失一部分元信息，导致乱码出现，这也算是处理多字符编码时必须注意的点
                                 */
                                byteBuffer.compact();
                                break;
                            } else if (result.isError()) {
                                System.err.printf("CharBuffer其它结果:%s\n", result);
                                result.throwException();
                            }
                        }
                        // 检查是否读取完毕（例如HTTP请求以空行结束）
                        if (requestBuilder.toString().contains("\r\n\r\n")) {
                            break;
                        }
                    }
                }
                /**
                 * 处理最后的数据，因为charbuffer不是以单个字节为处理单元，所以可能会暂存未解码的数据
                 */
                byteBuffer.flip();
                decoder.decode(byteBuffer, charBuffer, true); // 状态机扭转，结束输入
                // 强制刷新解码器
                decoder.flush(charBuffer);
                charBuffer.flip();
                requestBuilder.append(charBuffer);
                charBuffer.clear();

                String request = requestBuilder.toString();
                System.out.println("Received request:\n" + request);
                // 简单解析HTTP请求 HTTP GET /static/entry.html
                String[] requestLines = request.split("\r\n");
                if (requestLines.length > 0) {
                    String[] requestParts = requestLines[0].split(" ");
                    if (requestParts.length >= 2) {
                        String method = requestParts[0];
                        String path = requestParts[1];
                        // 检查连接是否仍然打开，否则会遇到ClosedChannelException，
                        // 实际上创建连接后，双方在内存中都维护了两个TCP状态机
                        synchronized (clientChannel) {
                            if (clientChannel.isOpen()) {
                                // 构建HTTP响应
                                String response = dispatchAndServ(method, path);
                                // 发送响应
                                ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8));
                                /**
                                 * 考虑到操作系统调用中缓冲区的广泛存在，所以采用循环写入
                                 * clientChannel.write(responseBuffer);
                                 */
                                while (responseBuffer.hasRemaining()) {
                                    clientChannel.write(responseBuffer);
                                }
                                // 检查是否需要保持连接
                                boolean keepAlive = isKeepAlive(request);
                                if (!keepAlive) {
                                    clientChannel.close();
                                    key.cancel();
                                }
                            }
                        }

                    }
                }
            } catch (IOException e) {
                // 处理IO异常
                try {
                    if (clientChannel.isOpen()) {
                        clientChannel.close();
                    }
                    key.cancel();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            } finally {
                // try {
                // clientChannel.close();
                // } catch (IOException ex) {
                // ex.printStackTrace();
                // }
            }
        });
    }

    // 检查HTTP Keep-Alive
    private boolean isKeepAlive(String request) {
        String[] lines = request.split("\r\n");
        for (String line : lines) {
            if (line.toLowerCase().startsWith("connection:")) {
                return line.toLowerCase().contains("keep-alive");
            }
        }
        return false;
    }

    /**
     * 本webserver支持的：
     * 1.以静态文档为主
     * 2.通过servlet生成json格式数据
     * 动态内容生成可以看mock.tomcat包下的Servlet服务器
     * 可以通过Servlet来生成动态内容，例如JSP技术，以及后端服务生成的json字符串
     * 这也是SpringBoot等框架通过Controller与Service的实现方案
     * 
     * @param method
     * @param path
     * @return
     */
    private String dispatchAndServ(String method, String path) {
        String responseContent = null;
        String rawContent = null;
        String resourcekey = String.format("%s-%s", path, method);
        WebConfigEntry mappingConfig = this.urlMapping.getOrDefault(resourcekey, null);
        if (mappingConfig == null) {
            responseContent = buildResponseString(null, path);
        } else {
            System.out.printf("请求key:%s，对应webconfig：%s\n", resourcekey, mappingConfig);
            // 动态生成json内容
            if (mappingConfig.dynamic) {
                // 相当于用一个结构体指针读取
                GenericService controller = (GenericService) controllerMap.getOrDefault(resourcekey, null);
                if (controller == null) {
                    // 反射机制加载类，生成动态内容
                    try {
                        Class<GenericService> genericSerClass = (Class<GenericService>) Class
                                .forName(mappingConfig.controllerPath);
                        controller = genericSerClass.newInstance();
                    } catch (Exception e) {
                        System.err.printf("Controller类加载异常:%s\n", e.getMessage());
                        e.printStackTrace();
                    }
                    controllerMap.put(resourcekey, controller);
                }
                // 通过反射生成动态内容
                if (mappingConfig.controllerMethod.equals(GenericService.DoGetPrefix)) {
                    rawContent = controller.doGet();
                } else if (mappingConfig.controllerMethod.equals(GenericService.DoPostPrefix)) {
                    rawContent = controller.doPost();
                } else {
                    throw new IllegalAccessError();
                }
                responseContent = buildResponseString(rawContent, path);
                // 静态文件内容
            } else {
                // 读取静态文件
                rawContent = fileCache.getOrDefault(mappingConfig.serverResource, null);
                if (rawContent == null) {
                    // 从静态文件目录读取
                    rawContent = readContentFromDisk(mappingConfig.serverResource);
                    fileCache.put(mappingConfig.serverResource, rawContent);
                }
                responseContent = buildResponseString(rawContent, path);
            }
        }
        return responseContent;

    }

    /**
     * [Channel]
     * → [ByteBuffer] (原始字节数据)
     * → [CharsetDecoder] (字节→字符转换)
     * → [CharBuffer] (临时字符存储，自动扩容)
     * → [StringBuilder] (最终结果收集)
     * 
     * @param filePath 文件路径
     * @return 文件内容
     */
    public static String readContentFromDisk(String filePath) {
        StringBuilder fBuilder = new StringBuilder();
        Charset charset = StandardCharsets.UTF_8;
        // 设置解码器，避免抛出异常
        CharsetDecoder decoder = charset.newDecoder()
                // CharsetDecoder 有严格的状态流转：RESET → CODING → FLUSHED → END
                .onMalformedInput(CodingErrorAction.REPLACE)
                .onUnmappableCharacter(CodingErrorAction.REPLACE);
        CharBuffer charBuffer = CharBuffer.allocate(1024);
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try (RandomAccessFile reqFile = new RandomAccessFile(filePath, "r");
                FileChannel channel = reqFile.getChannel()) {
            /**
             * 根据文件实际编码调整，否则底层的read读出来的只是char序列，-128-127，需要进行一次解码
             * 另外，不能在这个地方急着进行解码，只当成字节序列处理，否则因为utf8多字节编码，出现截断问题
             */
            while (channel.read(byteBuffer) != -1) {
                byteBuffer.flip();
                /**
                 * 之所以要循环读取，是因为bytebuffer与charbuffer大小可能不匹配
                 * buffer三元组，position limit capacity
                 * position<limit
                 */
                while (byteBuffer.hasRemaining()) {
                    // 解码循环
                    CoderResult result = decoder.decode(byteBuffer, charBuffer, false);
                    if (result.isOverflow()) {
                        // 1. 先输出已解码内容
                        charBuffer.flip();
                        fBuilder.append(charBuffer); // 这里判断是ok的，因为是命中了isOverflow
                        charBuffer.clear();
                        // 2. 双倍扩容，触发条件判断
                        if (byteBuffer.remaining() > charBuffer.capacity()) {
                            charBuffer = CharBuffer.allocate(charBuffer.capacity() * 2);
                        }
                        // 3. 重新尝试解码
                        continue;
                    } else if (result.isUnderflow()) {
                        /**
                         * 从input中[position,limit]拷贝至[0,limit-position]，方便后续的decode
                         * 这里等待charbuffer满了之后再进行一次操作
                         * 为什么这里需要一次compact，并且循环处理后byteBuffer不能clear呢？
                         * 因为如果是单字符编码，例如英文，按单个字节截取不会出问题，但多个字节的编码，
                         * 如中文，可能存在一个字符分到两个缓冲区的情况，例如nio.html遇到的
                         * <p>
                         * <code>flip()</code> 方法主要完成如下两步：
                         * </p>
                         * ，在"两步"这个地方，字节数来到1024的倍数
                         * 然后后续的字符被截取到两个缓冲区中，如果不进行compact且不clear，这个“步的"编码(&#x6B65)就会
                         * 丢失一部分元信息，导致乱码出现，这也算是处理多字符编码时必须注意的点
                         */
                        byteBuffer.compact();
                        break;
                    } else if (result.isError()) {
                        System.err.printf("CharBuffer其它结果:%s\n", result);
                        result.throwException();
                    }
                }
                // byteBuffer.clear();
            }
            /**
             * 处理最后的数据，因为charbuffer不是以单个字节为处理单元，所以可能会暂存未解码的数据
             */
            byteBuffer.flip();
            decoder.decode(byteBuffer, charBuffer, true); // 状态机扭转，结束输入
            // 强制刷新解码器
            decoder.flush(charBuffer);
            charBuffer.flip();
            fBuilder.append(charBuffer);
            charBuffer.clear();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return fBuilder.toString();
    }

    /**
     * 生成响应内容
     * 
     * @param body 响应体
     * @param path 请求路径
     * @return
     */
    private static String buildResponseString(String body, String path) {
        String statusLine = "HTTP/1.1 200 OK\r\n";
        String contentType = "Content-Type: text/html; charset=UTF-8\r\n";
        String contentLength;
        if (body == null) {
            body = "<html><body><h1>404 Not Found</h1><p>No resource at " + path + "</p></body></html>";
            statusLine = "HTTP/1.1 404 Not Found\r\n";
        }
        contentLength = "Content-Length: " + body.getBytes(StandardCharsets.UTF_8).length + "\r\n";
        return statusLine + contentType + contentLength + "\r\n" + body;
    }

    public static void main(String[] args) {
        try {
            new NIOWebServer(8080, new NIOWebServer.WorkerConfigure(10, 15, 1000, 800)).start();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
